package org.apache.ocean.util;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;

import org.apache.commons.io.*;
import java.util.concurrent.*;
import java.math.*;
import java.util.logging.*;
import java.util.concurrent.locks.*;

/**
 * Fail-Safe sequence store implementation using the file system. Works by versioning
 * values of sequences and throwing away all versions, but the current and the previous one.
 *
 * @version $Id: FileSequence.java 493628 2007-01-07 01:42:48Z joerg $
 */
public class FileSequence {
  protected final String storeDir;
  ReentrantLock writeLock = new ReentrantLock();
  public static final Logger logger = Logger.getLogger(FileSequence.class.getName());
  
  /**
   * Creates a new resouce manager operation on the specified directories.
   *
   * @param storeDir directory where sequence information is stored
   * @param logger logger used for warnings only
   */
  public FileSequence(String storeDir) throws ResourceManagerException {
    this.storeDir = storeDir;
    File file = new File(storeDir);
    file.mkdirs();
    if (!file.exists()) {
      throw new ResourceManagerException("Can not create working directory " + storeDir);
    }
  }
  
  /**
   * Checks if the sequence already exists.
   *
   * @param sequenceName the name of the sequence you want to check
   * @return <code>true</code> if the sequence already exists, <code>false</code> otherwise
   */
  public synchronized boolean exists(String sequenceName) {
    String pathI = getPathI(sequenceName);
    String pathII = getPathII(sequenceName);
    return (FileHelper.fileExists(pathI) || FileHelper.fileExists(pathII));
  }
  
  /**
   * Creates a sequence if it does not already exist.
   *
   * @param sequenceName the name of the sequence you want to create
   * @return <code>true</code> if the sequence has been created, <code>false</code> if it already existed
   * @throws ResourceManagerException if anything goes wrong while accessing the sequence
   */
  public synchronized boolean create(String sequenceName, BigInteger initialValue) throws ResourceManagerException {
    if (exists(sequenceName))
      return false;
    write(sequenceName, initialValue);
    return true;
  }
  
  /**
   * Deletes a sequence if it exists.
   *
   * @param sequenceName the name of the sequence you want to delete
   * @return <code>true</code> if the sequence has been deleted, <code>false</code> if not
   */
  public synchronized boolean delete(String sequenceName) {
    if (!exists(sequenceName))
      return false;
    String pathI = getPathI(sequenceName);
    String pathII = getPathII(sequenceName);
    
    // XXX be careful no to use shortcut eval with || might not delete second file
    boolean res1 = FileHelper.deleteFile(pathI);
    boolean res2 = FileHelper.deleteFile(pathII);
    
    return (res1 || res2);
  }
  
  /**
   * Gets the next value of the sequence.
   *
   * @param sequenceName the name of the sequence you want the next value for
   * @param increment the increment for the sequence, i.e. how much to add to the sequence with this call
   * @return the next value of the sequence <em>not yet incremented</em>, i.e. the increment is recorded
   * internally, but not returned with the next call to this method
   * @throws ResourceManagerException if anything goes wrong while accessing the sequence
   */
  public synchronized BigInteger nextSequenceValueBottom(String sequenceName, long increment) throws ResourceManagerException {
    writeLock.lock();
    try {
      if (!exists(sequenceName)) {
        throw new ResourceManagerException("Sequence " + sequenceName + " does not exist");
      }
      if (increment <= 0) {
        throw new IllegalArgumentException("Increment must be greater than 0, was " + increment);
      }
      BigInteger value = read(sequenceName);
      BigInteger newValue = value.add(new BigInteger(Long.toString(increment)));
      write(sequenceName, newValue);
      return value;
    } finally {
      writeLock.unlock();
    }
  }
  
  public synchronized BigInteger setSequenceValue(String sequenceName, BigInteger value) throws ResourceManagerException {
    writeLock.lock();
    try {
      if (!exists(sequenceName)) {
        throw new ResourceManagerException("Sequence "+sequenceName+" does not exist");
      }
      if (value.compareTo(new BigInteger("0")) < 0) {
        throw new IllegalArgumentException("value must be greater than 0, was "+value);
      }
      write(sequenceName, value);
      return value;
    } finally {
      writeLock.unlock();
    }
  }
  
  public BigInteger read(String sequenceName) throws ResourceManagerException {
    String pathI = getPathI(sequenceName);
    String pathII = getPathII(sequenceName);
    
    BigInteger returnValue = new BigInteger("-1");
    
    BigInteger valueI = new BigInteger("-1");
    if (FileHelper.fileExists(pathI)) {
      try {
        valueI = readFromPath(pathI);
      } catch (NumberFormatException e) {
        throw new ResourceManagerException("Fatal internal error: Backup sequence value corrupted");
      } catch (FileNotFoundException e) {
        throw new ResourceManagerException("Fatal internal error: Backup sequence vanished");
      } catch (IOException e) {
        throw new ResourceManagerException("Fatal internal error: Backup sequence value corrupted");
      }
    }
    
    BigInteger valueII = new BigInteger("-1");
    if (FileHelper.fileExists(pathII)) {
      try {
        valueII = readFromPath(pathII);
        //if (valueII > valueI) {
        if (valueII.compareTo(valueI) > 0) {
          returnValue = valueII;
        } else {
          // if it is smaller than previous this *must* be an error as we constantly increment
          logger.warning("Latest sequence value smaller than previous, reverting to previous");
          FileHelper.deleteFile(pathII);
          returnValue = valueI;
        }
      } catch (NumberFormatException e) {
        logger.warning("Latest sequence value corrupted, reverting to previous");
        FileHelper.deleteFile(pathII);
        returnValue = valueI;
      } catch (FileNotFoundException e) {
        logger.warning("Can not find latest sequence value, reverting to previous");
        FileHelper.deleteFile(pathII);
        returnValue = valueI;
      } catch (IOException e) {
        logger.warning("Can not read latest sequence value, reverting to previous");
        FileHelper.deleteFile(pathII);
        returnValue = valueI;
      }
    } else {
      logger.warning("Can not read latest sequence value, reverting to previous");
      returnValue = valueI;
    }
    
    if (!returnValue.toString().equals("-1")) {
      return returnValue;
    } else {
      throw new ResourceManagerException("Fatal internal error: Could not compute valid sequence value");
    }
  }
  
  protected void write(String sequenceName, BigInteger value) throws ResourceManagerException {
    String pathII = getPathII(sequenceName);
    
    File f2 = new File(pathII);
    // by contract when this method is called an f2 exists it must be valid
    if (f2.exists()) {
      // move previous value to backup position
      String pathI = getPathI(sequenceName);
      File f1 = new File(pathI);
      f1.delete();
      if (!f2.renameTo(f1)) {
        throw new ResourceManagerException("Fatal internal error: Can not create backup value at" + pathI);
      }
    }
    try {
      if (!f2.createNewFile()) {
        throw new ResourceManagerException("Fatal internal error: Can not create new value at" + pathII);
      }
    } catch (IOException e) {
      throw new ResourceManagerException("Fatal internal error: Can not create new value at" + pathII, e);
    }
    writeToPath(pathII, value);
  }
  
  protected String getPathI(String sequenceName) {
    return storeDir + "/" + sequenceName + "_1.seq";
  }
  
  protected String getPathII(String sequenceName) {
    return storeDir + "/" + sequenceName + "_2.seq";
  }
  
  protected BigInteger readFromPath(String path)
  throws ResourceManagerException, NumberFormatException, FileNotFoundException, IOException {
    File file = new File(path);
    BufferedReader reader = null;
    try {
      InputStream is = new FileInputStream(file);
      
      // we do not care for encoding as we only have numbers
      reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
      String valueString = reader.readLine();
      //long value = Long.parseLong(valueString);
      return new BigInteger(valueString);
    } catch (UnsupportedEncodingException e) {
      throw new ResourceManagerException("Fatal internal error, encoding UTF-8 unknown");
    } finally {
      if (reader != null) {
        try {
          reader.close();
        } catch (IOException e) {
        }
      }
    }
  }
  
  protected void writeToPath(String path, BigInteger value) throws ResourceManagerException {
    File file = new File(path);
    BufferedWriter writer = null;
    try {
      OutputStream os = new FileOutputStream(file);
      writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
      String valueString = value.toString();//Long.toString(value);
      writer.write(valueString);
      writer.write('\n');
    } catch (FileNotFoundException e) {
      throw new ResourceManagerException("Fatal internal error: Can not find sequence at " + path);
    } catch (IOException e) {
      throw new ResourceManagerException("Fatal internal error: Can not write to sequence at " + path);
    } finally {
      if (writer != null) {
        try {
          writer.close();
        } catch (IOException e) {
        }
      }
    }
  }
}
